package mmi.gui.shape;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

import mmi.gui.Application;

/**
 * Abstract shape base class. It contains some functions to create a proper derived class, as well
 * as some string representations.
 * 
 * @author louis
 *
 */
public abstract class Shape {

	/*
	 * 
	 */
	public static enum ShapePart {Border, Body};

	/*
	 * The shapes available. None is not actually a shape, but it's used for internal
	 * usage, for situations where no shape is meant.
	 */
	public static enum Shapes {None, Rectangle, Ellipse, Line, FreeLine, Triangle, Text};

	/*
	 * Sides are useful for resizing. When i.e. the top right corner is grabbed, you want to resize
	 * both right and top side. When you just grab the right side, you want to keep the bottom and
	 * top sides the way they are, but you just want to extend the item to the right.
	 */
	public static enum Sides {Top, TopRight, Right, BelowRight, Below, BelowLeft, Left, TopLeft};

	/*
	 * You can keep track of shapes with their id. This is useful in case of a replay, or just seeing
	 * which shapes belong together to one meta-shape (like e.g., free lines exist of several loose
	 * points).
	 */
	private static int currentId = 0;
	/**
	 * If for some reason you do know which shape you want to create (you know parameter shape), but
	 * the creation of the shape must be dynamically, use this function. It will yield a Shape with
	 * the proper derived type
	 * 
	 * This function doesn't create a real new object, it uses properties of the old one, which preserves
	 * things like the id.
	 * @param s the original shape, contains all the properties that must be saved. If null, a new object will
	 * be created
	 * @param shape
	 * @param start
	 * @param end
	 * @param width
	 * @return
	 */
	public static Shape createShape(Shape s, Shapes shape, Point2D start, Point2D end, Color lineColour, Color fillColour, int lineWidth)
	{
		Shape someShape = null;

		if (s == null) {
			switch (shape) {
			case Rectangle:
				someShape = new Rectangle(start, end, lineColour, fillColour, lineWidth);
				break;
			case Ellipse:
				someShape = new Ellipse(start, end, lineColour, fillColour, lineWidth);
				break;
			case Line:
				someShape = new Line(start, end, lineColour, fillColour, lineWidth);
				break;
			case FreeLine:
				someShape = new FreeLine(start, end, lineColour, lineWidth);
				break;
			case Triangle:
				someShape = new Triangle(start, null, end, lineColour, fillColour, lineWidth);
				break;
			case Text:
				//				someShape = new TextObject(start, new StringAsker().getValue());
				break;
			default:
				System.err.println("Unidentified shape found!");
				break;
			}
		} else
		{
			switch (shape) {
			case Rectangle:
				someShape = new Rectangle(s.getId(), start, end, lineColour, fillColour, lineWidth);
				break;
			case Ellipse:
				someShape = new Ellipse(s.getId(), start, end, lineColour, fillColour, lineWidth);
				break;
			case Line:
				someShape = new Line(s.getId(), start, end, lineColour, fillColour, lineWidth);
				break;
			case FreeLine:
				someShape = new FreeLine(s.getId(), start, end, lineColour, lineWidth);
				break;
			case Triangle:
				someShape = new Triangle(s.getId(), start, end, s.getMiddle(), lineColour, fillColour, lineWidth);
				break;
			case Text:
				Application.TraceDebug(s.getId() + ((TextObject) s).getText());
				someShape = new TextObject(s.getId(), start, ((TextObject) s).getText(), ((TextObject) s).getFont(), lineColour);
				
				break;
			default:
				System.err.println("Unidentified shape found!");
				break;
			}
		}

		return someShape;
	}

	/**
	 * Creating a new object
	 * @see Shape createShape(Shape s, Shapes shape, Point2D start, Point2D end, double width)
	 */
	public static Shape createShape(Shapes shape, Point2D start, Point2D end, Color lineColour, Color fillColour, int lineWidth)
	{
		Shape s = null;

		return createShape(s, shape, start, end, lineColour, fillColour, lineWidth);
	}

	/**
	 * @return the boundingBoxSize
	 */
	public static int getBoundingBoxSize() {
		return boundingBoxSize;
	}

	/**
	 * For some shapes a custom contain-function will have to be implemented. Using the same contain
	 * radius for every custom shape, will give all the shapes the same "look and feel"
	 * @return the containRadius
	 */
	public static int getContainRadius() {
		return containRadius;
	}

	/**
	 * @return the currentId
	 */
	public static int getCurrentId() {
		return currentId;
	}

	/**
	 * If you know the name of a shape, but don't know the internal representation,
	 * use this function.
	 * @param shapeName a string representation of a shape
	 * @return the corresponding shape
	 */
	public static Shapes getShape(String shapeName)
	{
		Shapes s = null;

		if(shapeName.equals("Rectangle"))
		{
			s = Shapes.Rectangle;
		} else if(shapeName.equals("Ellipse"))
		{
			s = Shapes.Ellipse;
		} else if(shapeName.equals("Line"))
		{
			s = Shapes.Line;
		} else if(shapeName.equals("FreeLine"))
		{
			s = Shapes.FreeLine;
		} else if(shapeName.equals("Triangle"))
		{
			s = Shapes.Triangle;
		} else if(shapeName.equals("Text"))
		{
			s = Shapes.Text;
		}

		return s;
	}

	/**
	 * Use this function when you want to give a shape an id. It makes sure the internal administration
	 * will stay valid, and in case the way ids are given away chances, this function acts as a nice
	 * wrapper.
	 * @return the highest not yet given id
	 */
	private static int newId()
	{
		return currentId++;
	}

	/**
	 * @param boundingBoxSize the boundingBoxSize to set
	 */
	public static void setBoundingBoxSize(int boundingBoxSize) {
		Shape.boundingBoxSize = boundingBoxSize;
	}

	/**
	 * @param containRadius the containRadius to set
	 */
	public static void setContainRadius(int containRadius) {
		Shape.containRadius = containRadius;
	}

	/**
	 * @param currentId the currentId to set
	 */
	public static void setCurrentId(int currentId) {
		Shape.currentId = currentId;
	}

	protected final int id;

	protected Point2D start;

	protected Point2D middle = new Point2D.Double(80,80);

	protected Point2D end;

	protected int lineWidth = 2;

	protected Color lineColour = Color.black;

	protected Color fillColour = null;

	protected boolean showBoundingLines = false;

	protected boolean showBoundingBox = false;

	private static int boundingBoxSize = 10;

	private static int containRadius = 2;

	/**
	 * This constructor should only be used in case of copying shapes, for example when you load a replay.
	 * Just giving an id might conflict with currentId. Therefor when reading in some action from a file, this will
	 * have to be thought about again
	 * @see Shape(Point2D start, Point2D end, double width)
	 */
	protected Shape(int id, Point2D start, Point2D end)
	{
		this.id = id;
		this.start = start;
		this.end = end;
	}

	/**
	 * This constructor should only be used in case of copying shapes, for example when you load a replay.
	 * Just giving an id might conflict with currentId. Therefor when reading in some action from a file, this will
	 * have to be thought about again. This is the colour edition.
	 * @see Shape(Point2D start, Point2D end, double width)
	 */
	protected Shape(int id, Point2D start, Point2D end, Color lineColour, Color fillColour, int lineWidth)
	{
		this.id = id;
		this.start = start;
		this.end = end;
		this.fillColour = fillColour;
		this.lineColour = lineColour;
		this.lineWidth = lineWidth;
	}

	/**
	 * "Default" constructor. It creates a new shape, and makes sure the administration for the
	 * inherited objects gets done.
	 * @param start
	 * @param end
	 * @param width
	 */
	protected Shape(Point2D start, Point2D end)
	{
		id = newId();
		this.start = start;
		this.end = end;
	}

	/**
	 * "Default" constructor. It creates a new shape, and makes sure the administration for the
	 * inherited objects gets done. This is the colour edition.
	 * @param start
	 * @param end
	 * @param width
	 */
	protected Shape(Point2D start, Point2D end, Color lineColour, Color fillColour, int lineWidth)
	{
		id = newId();
		this.start = start;
		this.end = end;
		this.fillColour = fillColour;
		this.lineColour = lineColour;
		this.lineWidth = lineWidth;
	}

	public abstract boolean contains(Point2D position);

	public abstract void draw(Graphics2D g);

	/**
	 * Draws a default bounding box. The default is to place 4 bounding boxes, in each corner one.
	 * For special bounding boxes per shape, override this function.
	 * @param g
	 */
	public void drawBoundingBox(Graphics2D g)
	{
		if(showBoundingBox)
		{
			int leftX = (int) (start.getX() <= end.getX() ? start.getX() : end.getX());
			int rightX = (int) (start.getX() <= end.getX() ? end.getX() : start.getX());

			int topY = (int) (start.getY() <= end.getY() ? start.getY() : end.getY());
			int bottomY = (int) (start.getY() <= end.getY() ? end.getY() : start.getY());

			int halfWidth = (rightX - leftX) / 2 + leftX;
			int halfHeight = (bottomY - topY) / 2 + topY;

			// top left
			g.fillRect(leftX, topY, Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());

			// top
			g.fillRect((int) (halfWidth - .5*Shape.getBoundingBoxSize()), topY, Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());

			// top right
			g.fillRect(rightX - Shape.getBoundingBoxSize(), topY, Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());

			// right
			g.fillRect(rightX - Shape.getBoundingBoxSize(), (int) (halfHeight - 0.5*Shape.getBoundingBoxSize()), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());

			// bottom right
			g.fillRect(rightX - Shape.getBoundingBoxSize(), bottomY - Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());

			// bottom
			g.fillRect((int) (halfWidth - .5*Shape.getBoundingBoxSize()), bottomY - Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());

			// bottom left
			g.fillRect(leftX, bottomY - Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());

			// left
			g.fillRect(leftX, (int) (halfHeight - 0.5*Shape.getBoundingBoxSize()), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());

			Stroke defaultStroke = g.getStroke();
			g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{9}, 0));

			// upper line
			g.drawLine(leftX, topY,
					rightX, topY);

			// right line
			g.drawLine(rightX, topY,
					rightX, bottomY);

			// bottom line
			g.drawLine(rightX, bottomY,
					leftX, bottomY);

			// left line
			g.drawLine(leftX, bottomY,
					leftX, topY);

			g.setStroke(defaultStroke);
		}
	}

	public abstract void fill(Graphics2D g);

	/**
	 * @return the end
	 */
	public Point2D getEnd() {
		return end;
	}

	public Color getFillColour()
	{
		return fillColour;
	}

	/**
	 * @return the id
	 */
	public int getId() {
		return id;
	}

	public Color getLineColour()
	{
		return lineColour;
	}

	/**
	 * @return the lineWidth
	 */
	public int getLineWidth() {
		return lineWidth;
	}

	public Point2D getMiddle()
	{
		return middle;
	}

	public abstract Shapes getShape();
	/**
	 * @return the start
	 */
	public Point2D getStart() {
		return start;
	}
	public abstract String getXMLRepresentation();

	/**
	 * @return the showBoundingBox
	 */
	public boolean isShowBoundingBox() {
		return showBoundingBox;
	}
	/**
	 * @return the showBoundingLines
	 */
	public boolean isShowBoundingLines() {
		return showBoundingLines;
	}

	/**
	 * Test if a position is corresponding to one of the bounding boxes
	 * @param position to test for a bounding box
	 * @return Side if position corresponds to a bounding box, otherwise null
	 */
	public Sides isSide(Point2D position)
	{

		int leftX = (int) (start.getX() <= end.getX() ? start.getX() : end.getX());
		int rightX = (int) (start.getX() <= end.getX() ? end.getX() : start.getX());

		int topY = (int) (start.getY() <= end.getY() ? start.getY() : end.getY());
		int bottomY = (int) (start.getY() <= end.getY() ? end.getY() : start.getY());

		int halfWidth = (rightX - leftX) / 2 + leftX;
		int halfHeight = (bottomY - topY) / 2 + topY;

		Rectangle2D topLeft = new Rectangle2D.Double(leftX, topY, Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());
		if(topLeft.contains(position))
		{
			Application.TraceDebug("TL side click detected");
			return Sides.TopLeft;
		}

		Rectangle2D topRight = new Rectangle2D.Double(rightX - Shape.getBoundingBoxSize(), topY, Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());
		if(topRight.contains(position))
		{
			Application.TraceDebug("TR side click detected");
			return Sides.TopRight;
		}

		Rectangle2D belowLeft= new Rectangle2D.Double(leftX, bottomY - Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());
		if(belowLeft.contains(position))
		{
			Application.TraceDebug("BL side click detected");
			return Sides.BelowLeft;
		}

		Rectangle2D belowRight = new Rectangle2D.Double(rightX - Shape.getBoundingBoxSize(), bottomY - Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());
		if(belowRight.contains(position))
		{
			Application.TraceDebug("BR side click detected");
			return Sides.BelowRight;
		}

		Rectangle2D top = new Rectangle2D.Double(halfWidth - .5*Shape.getBoundingBoxSize(), topY, Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());
		if(top.contains(position))
		{
			Application.TraceDebug("T side click detected");
			return Sides.Top;
		}

		Rectangle2D right = new Rectangle2D.Double(rightX - Shape.getBoundingBoxSize(), halfHeight - 0.5*Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());
		if(right.contains(position))
		{
			Application.TraceDebug("R side click detected");
			return Sides.Right;
		}

		Rectangle2D below = new Rectangle2D.Double(halfWidth - .5*Shape.getBoundingBoxSize(), bottomY - Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());
		if(below.contains(position))
		{
			Application.TraceDebug("B side click detected");
			return Sides.Below;
		}

		Rectangle2D left = new Rectangle2D.Double(leftX, halfHeight - 0.5*Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize(), Shape.getBoundingBoxSize());
		if(left.contains(position))
		{
			Application.TraceDebug("L side click detected");
			return Sides.Left;
		}

		return null;
	}
	public abstract void makeTheShape();

	public void moveShape(double xDif, double yDif)
	{
		start.setLocation(start.getX() - xDif, start.getY() - yDif);
		middle.setLocation(middle.getX() - xDif, middle.getY() - yDif);
		end.setLocation(end.getX() - xDif, end.getY() - yDif);

		makeTheShape();
	}
	public void resizeShape(Shape.Sides side, Double optionalXDif, Double optionalYDif)
	{
		double xdif = optionalXDif == null ? 0 : optionalXDif;
		double ydif = optionalYDif == null ? 0 : optionalYDif;

		switch (side) {
		case BelowRight:
			end.setLocation(end.getX() + xdif, end.getY() + ydif);
			break;
		case BelowLeft:
			start.setLocation(start.getX() + xdif, start.getY());
			end.setLocation(end.getX(), end.getY() + ydif);
			break;
		case TopLeft:
			start.setLocation(start.getX() + xdif, start.getY() + ydif);
			break;
		case TopRight:
			start.setLocation(start.getX(), start.getY() + ydif);
			end.setLocation(end.getX() + xdif, end.getY());
			break;
		case Top:
			start.setLocation(start.getX(), start.getY() + ydif);
			break;
		case Right:
			end.setLocation(end.getX() + xdif, end.getY());
			break;
		case Below:
			end.setLocation(end.getX(), end.getY() + ydif);
			break;
		case Left:
			start.setLocation(start.getX() + xdif, start.getY());
		default:
			break;
		}

		makeTheShape();
	}
	/**
	 * @param end the end to set
	 */
	public void setEnd(Point2D end) {
		this.end = end;
	}
	public void setFillColour(Color colour)
	{
		fillColour = colour;
	}

	public void setLineColour(Color colour)
	{
		lineColour = colour;
	}

	/**
	 * @param lineWidth the lineWidth to set
	 */
	public void setLineWidth(int lineWidth) {
		this.lineWidth = lineWidth;
	}
	public void setMiddle(Point2D middle)
	{
		this.middle = middle;
	}

	/**
	 * @param showBoundingBox the showBoundingBox to set
	 */
	public void setShowBoundingBox(boolean showBoundingBox) {
		this.showBoundingBox = showBoundingBox;
	}
	/**
	 * @param showBoundingLines the showBoundingLines to set
	 */
	public void setShowBoundingLines(boolean showBoundingLines) {
		this.showBoundingLines = showBoundingLines;
	}
	/**
	 * @param start the start to set
	 */
	public void setStart(Point2D start) {
		this.start = start;
	}
	@Override
	public abstract String toString();
}
